﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;


namespace rossisdead.Mvc.Detergent
{
    public interface ICascadingDropDownExtensionsHelper
    {
        /// <summary>
        /// Applies a cascading html attribute if the value is not null, empty, or white space.
        /// The value is html encoded.
        /// </summary>
        void ApplyOptionalHtmlAttribute(string value, string attrName, IDictionary<string, object> htmlAttributes);

        void ApplyCascadingHtmlAttributes(CascadingAttribute cascade, IDictionary<string, object> htmlAttributes);
        RouteData GetRouteData(string actionPath);
        IEnumerable<SelectListItem> GetSelectListItems(HtmlHelper htmlHelper, CascadingAttribute cascade);
        CascadingAttribute GetCascadingAttribute(HtmlHelper htmlHelper, string propertyName);
        ModelMetadata GetModelMetadata(HtmlHelper htmlHelper, string propertyName);
    }

    /// <summary>
    /// Helper class for use with the CascadingDropDownExtensions. Allows for the Helper property instance
    /// to be swappable.
    /// 
    /// Most of the code in here relates to being used with the jquery.detergent-1.0 script.
    /// </summary>
    public class CascadingDropDownExtensionsHelper : ICascadingDropDownExtensionsHelper
    {
        #region Private methods

        /// <summary>
        /// Invokes the public instance method with the given name on the
        /// given object, using the given parameters.
        /// </summary>
        /// <returns>The return value from invoking the specified method.</returns>
        private static object InvokePublicInstanceMethod(object obj, string methodName, params object[] parameters)
        {
            const BindingFlags publicInstance = BindingFlags.Instance | BindingFlags.Public;
            var mi = obj.GetType().GetMethod(methodName, publicInstance);

            if (mi == null)
            {
                var message = string.Format("Could not find public method '{0}' on type '{1}'",
                                            methodName, obj.GetType().Name);
                throw new InvalidOperationException(message);
            }


            return mi.Invoke(obj, parameters);
        }

        public static object GetPropertyValueByName(object obj, string propertyName)
        {
            if (obj == null)
            {
                throw new ArgumentNullException("obj");
            }

            var propInfo = obj.GetType().GetProperty(propertyName);
            if (propInfo == null)
            {
                var message = string.Format("Could not find property '{0}' on type '{1}'", propertyName, obj.GetType().Name);
                throw new InvalidOperationException(message);
            }

            return propInfo.GetValue(obj, null);
        }

        #endregion

        /// <summary>
        /// Applies a cascading html attribute if the value is not null, empty, or white space.
        /// The value is html encoded.
        /// </summary>
        public virtual void ApplyOptionalHtmlAttribute(string value, string attrName, IDictionary<string, object> htmlAttributes)
        {
            if (!string.IsNullOrWhiteSpace(value))
            {
                htmlAttributes[attrName] = HttpUtility.HtmlAttributeEncode(value);
            }
        }

        public virtual void ApplyCascadingHtmlAttributes(CascadingAttribute cascade, IDictionary<string, object> htmlAttributes)
        {
            // This needs to be the lowercased "true". If the value is set to bool true, it renders it as "True"
            htmlAttributes["data-cascading"] = "true";

            // Html encoding isn't done automatically, so we need to do it ourselves. 
            htmlAttributes["data-cascading-action"] = HttpUtility.HtmlEncode(cascade.Action);
            htmlAttributes["data-cascading-dependson"] = HttpUtility.HtmlEncode("#" + cascade.DependsOn);
            ApplyOptionalHtmlAttribute(cascade.ErrorText, "data-cascading-errortext", htmlAttributes);
            ApplyOptionalHtmlAttribute(cascade.LoadingText, "data-cascading-loadingtext", htmlAttributes);
            ApplyOptionalHtmlAttribute(cascade.PromptText, "data-cascading-prompttext", htmlAttributes);
            ApplyOptionalHtmlAttribute(cascade.HttpMethod, "data-cascading-httpmethod", htmlAttributes);

            if (!cascade.Async.GetValueOrDefault())
            {
                htmlAttributes["data-cascading-async"] = "false"; // Same as above with the "true" thing
            }
        }

        public virtual RouteData GetRouteData(string actionPath)
        {
            // This url needs to be absolute for the GetRouteData extension to work with querystrings.
            var uri = new Uri("http://somesite/" + actionPath);
            var routeData = RouteTable.Routes.GetRouteData(uri);
            return routeData;
        }

        private CascadeValue GetValues(HtmlHelper htmlHelper, CascadingAttribute cascade)
        {
            var viewData = htmlHelper.ViewData;
            object parentVal = null;
            object childVal = null;

            // This means our model isn't coming from an EditorFor template.
            if (viewData.ModelMetadata.IsComplexType)
            {
                // If it's null, we can't do anything with setting values here.
                if (viewData.Model != null)
                {
                    parentVal = GetPropertyValueByName(viewData.Model, cascade.DependsOn);
                    childVal = GetPropertyValueByName(viewData.Model, cascade.OwningProperty);
                }
            }
            else
            {
                // We need to do a key check vs a null check
                // because the model's allowed to be null,
                // but it can't not be set. 
                if (!viewData.ContainsKey("Container"))
                {
                    throw new InvalidOperationException(
                        "CascadingDropDownExtensions require that the parent model be passed in with as additional ViewData with the key 'Container'. This can be done from the view.");
                }

                parentVal = GetPropertyValueByName(viewData["Container"], cascade.DependsOn);
                // We're inside an EditorFor template, so the model *is* our child value.
                childVal = viewData.Model;
            }

            return new CascadeValue(parentVal, childVal);
        }

        public virtual IEnumerable<SelectListItem> GetSelectListItems(HtmlHelper htmlHelper, CascadingAttribute cascade)
        {
            var values = GetValues(htmlHelper, cascade);

            if (values.ParentValue == null)
            {
                // Our DependsOn has no value, so we can't prepopulate the values.
                // Also we can't set the selected value since we can't verify that
                // it's still valid.
                return Enumerable.Empty<SelectListItem>();
            }
            var routeData = GetRouteData(cascade.Action);
            var controller = ControllerBuilder.Current.GetControllerFactory().CreateController(htmlHelper.ViewContext.RequestContext,
                                                                              routeData.Values["controller"].ToString());

            try
            {
                var result = (CascadingActionResult)InvokePublicInstanceMethod(controller, (string)routeData.Values["action"], values.ParentValue);
                result.SelectedValue = values.ChildValue;
                return result.GetSelectListItems();
            }
            finally
            {
                // Make sure controller gets disposed. 
                // System.Web.Mvc.Controller is IDisposable, but IController is not.
                if (controller is IDisposable)
                {
                    ((IDisposable)controller).Dispose();
                }
            }
        }


        public virtual CascadingAttribute GetCascadingAttribute(HtmlHelper htmlHelper, string propertyName)
        {
            return CascadingAttribute.GetAttributeForModel(GetModelMetadata(htmlHelper, propertyName));
        }

        public virtual ModelMetadata GetModelMetadata(HtmlHelper htmlHelper, string propertyName)
        {
            // This might not work in the case of really complex model types(ie, complex type within a complex type).
            // If Html.CascadingDropDown is called from outside of a template, the ModelMetadata that's passed in
            // is for the view's model, not the property itself, so we need to find that.
            var meta = htmlHelper.ViewData.ModelMetadata;
            if (meta != null)
            {
                while (meta.IsComplexType)
                {
                    meta = meta.Properties.SingleOrDefault(x => x.PropertyName == propertyName);
                    if (meta == null) { break; }
                }
            }

            if (meta == null)
            {
                throw new InvalidOperationException("Unable to locate metadata for model '" + propertyName + "'");
            }

            return meta;
        }

        #region Helper classes

        private struct CascadeValue
        {
            public object ParentValue { get; private set; }
            public object ChildValue { get; private set; }

            public CascadeValue(object pv, object cv) : this()
            {
                ParentValue = pv;
                ChildValue = cv;
            }

        }

        #endregion

    }
}